package main

import (
	"os"
	"fmt"
	"io/ioutil"
	"path/filepath"
)

//WalkSkipDir is the Error returned when we want to skip descending into a directory
var WalkSkipDir = os.NewError("skip this directory")

//WalkFunc is a callback function called for each path as a directory is walked
//If resolvedPath != "", then we are following symbolic links.
type WalkFunc func(path string, resolvedPath string, info *os.FileInfo, err os.Error) os.Error

//Walk walks a path, optionally following symbolic links, and for each path,
//it calls the walkFn passed. 
//
//It is similar to filepath.Walk, except that it supports symbolic links and 
//can detect infinite loops while following sym links. 
//It solves the issue where your WalkFunc needs a path relative to the symbolic link 
//(resolving links within walkfunc loses the path to the symbolic link for each traversal).
func Walk(path string, followSymlinks bool, detectSymlinkInfiniteLoop bool, walkFn WalkFunc) (
	os.Error) {
	info, err := os.Lstat(path)
	if err != nil { return err }
	var symlinkPathsFollowed map[string]bool
	var resolvedPath string
	if followSymlinks {
		resolvedPath = path
		if detectSymlinkInfiniteLoop { symlinkPathsFollowed = make(map[string]bool, 8) }
	}
	return walk(path, info, resolvedPath, symlinkPathsFollowed, walkFn)
}

//walk walks the path. It is a helper/sibling function to Walk.
//It takes a resolvedPath into consideration. This way, paths being walked are 
//always relative to the path argument, even if symbolic links were resolved).
//
//If resolvedPath is "", then we are not following symbolic links.
//If symlinkPathsFollowed is not nil, then we need to detect infinite loop.
func walk(path string, info *os.FileInfo, resolvedPath string, 
	symlinkPathsFollowed map[string]bool, walkFn WalkFunc) os.Error {
	if info == nil { return os.NewError("Walk: Nil FileInfo passed") }
	err := walkFn(path, resolvedPath, info, nil)
	if err != nil {
		if info.IsDirectory() && err == WalkSkipDir { err = nil }
		return err
	}
	if resolvedPath != "" && info.IsSymlink() {
		path2, err := os.Readlink(resolvedPath)
		if err != nil { return err }
		//vout("SymLink Path: %v, links to: %v", resolvedPath, path2)
		if symlinkPathsFollowed != nil {
			if _, ok := symlinkPathsFollowed[path2]; ok {
				errMsg := "Potential SymLink Infinite Loop. Path: %v, Link To: %v"
				return fmt.Errorf(errMsg, resolvedPath, path2)
			} else {
				symlinkPathsFollowed[path2] = true
			}
		}
		info2, err := os.Lstat(path2)
		if err != nil { return err }
		return walk(path, info2, path2, symlinkPathsFollowed, walkFn)
	} 
	if info.IsDirectory() { 
		list, err := ioutil.ReadDir(path)
		if err != nil { return walkFn(path, resolvedPath, info, err) }
		for _, fileInfo := range list {
			path2 := filepath.Join(path, fileInfo.Name)
			var resolvedPath2 string
			if resolvedPath != "" { resolvedPath2 = filepath.Join(resolvedPath, fileInfo.Name) }
			err = walk(path2, fileInfo, resolvedPath2, symlinkPathsFollowed, walkFn)
			if err != nil { return err }
		}
		return nil
	}
	return nil
}

